package com.xzzz.common.base.util;

import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.function.Predicate;

/**
 * 邀请码生成器, 支持靓号生成
 *
 * @author : xzzz
 */
@SuppressWarnings("all")
public class InvCodeUtil {

    /**
     * 不包含0(零), 容易和字母O混淆
     */
    private static final char[] CODE_ITEM = {
            'A', 'B', 'C', 'D', 'E',
            'F', 'G', 'H', 'I', 'J',
            'K', 'L', 'M', 'N', 'O',
            'P', 'Q', 'R', 'S', 'T',
            'U', 'V', 'W', 'X', 'Y', 'Z',

            '1', '2', '3', '4', '5',
            '6', '7', '8', '9', // '0'
    };

    /**
     * 邀请码长度, 推荐在 8~10 位之间
     * <pre>{@code
     * 长度不同产生的碰撞概率如下 (生成的邀请码越多, 碰撞概率越大)
     *   邀请码位数  生成数     重复数     靓号
     *   6位邀请码 | 1000万 | ≈27000  | 9730  |
     *   7位邀请码 | 1000万 | ≈750    | 16870 |
     *   8位邀请码 | 1000万 | ≈30     | 16870 |
     *  10位邀请码 | 1000万 | ≈<3     | 22820 |
     * }</pre>
     */
    private static final int INV_CODE_LENGTH = 8;

    /**
     * CODE_ITEM 数组的长度, 用于确定随机数的区间
     * <p>如 RANDOM_RANGE = 35, 则会生成 0~34 的随机数
     */
    private static final int RANDOM_RANGE = CODE_ITEM.length;

    /**
     * 生成普通邀请码
     *
     * @return 邀请码
     */
    public static String getInvCode() {
        return getInvCode(false);
    }

    /**
     * 生成随机靓号邀请码
     *
     * @return 邀请码
     */
    public static String getGoodInvCode() {
        return getInvCode(true);
    }

    /**
     * 生成邀请码
     *
     * @param isGood 是否靓号
     * @return 邀请码
     */
    public static String getInvCode(boolean isGood) {
        if (isGood) {
            // 随机靓号模板
            int randomGood = new Random().nextInt(GoodCode.values().length - 1);
            return getInvCode(GoodCode.values()[randomGood]);
        } else {
            // 随机邀请码
            return getInvCode(GoodCode.RANDOM);
        }
    }

    /**
     * 批量生成邀请码
     *
     * @param isGood  是否良好
     * @param codeNum 批量的数量
     * @return 邀请码数组
     */
    public static String[] getInvCodes(boolean isGood, Integer codeNum) {
        if (codeNum == null) {
            codeNum = 1;
        }

        String[] codes = new String[codeNum];
        for (int i = 0; i < codes.length; i++) {
            codes[i] = getInvCode(isGood);
        }

        return codes;
    }

    /**
     * 按模板生成邀请码
     *
     * @param goodCode 靓号模板
     * @return 邀请码
     */
    public static String getInvCode(GoodCode goodCode) {
        int[] arr = genRandomArray(goodCode);
        StringBuilder invCode = new StringBuilder();
        for (int value : arr) {
            invCode.append(CODE_ITEM[value]);
        }
        return invCode.toString();
    }

    /**
     * 生成随机数数组, 作为 code_item 的下标
     *
     * @param goodCode 靓号模板
     * @return 随机数组成的数组
     */
    private static int[] genRandomArray(GoodCode goodCode) {
        if (INV_CODE_LENGTH < 5) {
            throw new RuntimeException("邀请码长度不得小于5");
        }
        int[] arr = new int[INV_CODE_LENGTH];
        switch (goodCode) {
            /**
             * 全部相同,这种靓号的个数与 CODE_ITEM 的长度相同
             */
            case AAA: {
                arr = getCodeArray(null);
                break;
            }
            /**
             * 尾部不同, 偶数包含左右平分的情况
             */
            case XXY: {
                int tail = INV_CODE_LENGTH / 2;
                int randomTail = new Random().nextInt(tail) + 1;
                arr = getCodeArray((i) -> INV_CODE_LENGTH - i <= randomTail);
                break;
            }
            /**
             * 头部不同, 偶数包含左右平分的情况
             */
            case YXX: {
                int head = INV_CODE_LENGTH / 2;
                int randomHead = new Random().nextInt(head) + 1;
                arr = getCodeArray((i) -> i >= randomHead);
                break;
            }
            /**
             * 左右不同, 奇数包含头部不同的情况,奇数的情况下即为 [ABB]
             */
            case XY: {
                arr = getCodeArray((i) -> i > INV_CODE_LENGTH / 2 - 1);
                break;
            }
            /**
             * 头尾与中间不同, 如果邀请码长度为偶数, 为了防止出现 [AB] 的情况, 头尾长度需要-1
             */
            case XYX: {
                int headTail = INV_CODE_LENGTH / 2;
                if (headTail % 2 == 0 && INV_CODE_LENGTH % 2 == 0) headTail--; // 排除掉 [AAABBB] 这种左右平均的组合
                int randomheadTail = new Random().nextInt(headTail) + 1;
                arr = getCodeArray((i) -> randomheadTail <= i && i < INV_CODE_LENGTH - randomheadTail);
                break;
            }
            /** 交叉不同
             * 1. 计算交叉组合的数量,交叉组合的数量,交叉组合至少需要3组,所以:
             *    组合数 = 邀请码长度 / 2
             *    如长度为5 ,则有 2 种情况:
             *      1. ABABA
             *      2. AABBA
             *         AAABB - 不包含,这种组合为2组,属于[AAB] 的情况
             *    如长度为13,则有 6 种情况:
             *      1. ABABABABABABA
             *      2. AABBAABBAABBA
             *      3. AAABBBAAABBBA
             *      4. AAAABBBBAAAAB
             *      5. AAAAABBBBBAAA
             *      6. AAAAAABBBBBBA - 6个一组
             *         AAAAAAABBBBBB - 不包含,这种组合为2组,属于[AAB]的情况
             *
             * 2. 遍历下标数组, 如果下标可以整除每组的个数, 说明当前下标的邀请码到了变更的时候,即如下变更
             * <code>
             *    if (i % actionGroup == 0) {
             *        if (T == B) {
             *            T = A;
             *        } else {
             *            T = B;
             *        }
             *    }
             *    arr[i] = T;                       // 正序组合
             *    arr[INV_CODE_LENGTH - 1 - i] = T; // 倒序组合
             * </code>
             *
             */
            case XYXY: {
                Random r = new Random();
                // 交叉组合的数量,交叉组合至少需要2个交叉,所以 / 2
                int groupNum = INV_CODE_LENGTH / 2;
                // 排除掉 [AAABBB] 这种左右平均的组合
                if (groupNum % 2 == 0 && INV_CODE_LENGTH % 2 == 0) groupNum--;
                // 分组的阈值,即每组长度,随机值从0开始,所以+1
                int actionGroup = r.nextInt(groupNum) + 1;
                int A = r.nextInt(RANDOM_RANGE);
                int B = reGen(A, r);
                int T = A;
                // 0:正序;1:倒序;
                int preorder = r.nextInt(2);
                for (int i = 0; i < INV_CODE_LENGTH; i++) {
                    // 循环邀请码数组下标的位数与当前分组的个数整除, 则说明需要更换数组中的值
                    if (i % actionGroup == 0) {
                        if (T == B) {
                            T = A;
                        } else {
                            T = B;
                        }
                    }
                    if (preorder == 0) {
                        arr[i] = T;
                    } else if (preorder == 1) {
                        arr[INV_CODE_LENGTH - 1 - i] = T;
                    }
                }
                break;
            }
            /**
             * 每组至少3个随机元素,最多5个元素,最多2组
             * 长度7, 分组4
             * 1 | ABC ABC A
             * 2 | ABCD ABC
             * 3 | ABCDE AB
             * 4 | ABCDEF A
             *
             * 长度8, 分组5
             * 1 | ABC ABC AB
             * 2 | ABCD ABCD
             * 3 | ABCDE ABC
             * 4 | ABCDEF AB
             * 5 | ABCDEFG A
             *
             * 长度9, 分组6
             * 1 | ABC ABC ABA
             * 2 | ABCD ABCD A
             * 3 | ABCDE ABCD
             * 4 | ABCDEF ABC
             * 5 | ABCDEFG AB
             * 6 | ABCDEFGH B
             *
             * 长度10, 分组7
             * 1 | ABC ABC ABC A
             * 2 | ABCD ABCD AB
             * 3 | ABCDE ABCDE
             * 4 | ABCEDF ABCD
             * 5 | ABCDEFG ABC
             * 6 | ABCDEFGH AB
             * 7 | ABCDEFGHI A
             */
//            case XYZXYZ: {
//                Random r = new Random();
//                // 交叉组合的数量,至少需要3个一组,2个一组的情况与[AB]相同
//                int groupNum = INV_CODE_LENGTH - 3;
//                // 分组的阈值,即每组长度,每组至少3个元素,最多2组,即[3-9]
//                int actionGroup = r.nextInt(groupNum) + 3;
//                // 每组的元素最多5个,多余5个不容易记忆,则没有靓号的意义
//                if (actionGroup > 4) {
//                    actionGroup = 4;
//                }
//                // 每组的元素,例如: [A,C,X]
//                int groupItem[] = new int[actionGroup];
//                groupItem[0] = r.nextInt(RANDOM_RANGE);
//                for (int i = 1; i < groupItem.length; i++) {
//                    groupItem[i] = reGen(groupItem[i-1],r);
//                }
//                int T = groupItem[0];
//                // 0:正序;1:倒序;
//                int preorder = r.nextInt(2);
//                int roll = 0; // groupItem的位置
//                for (int i = 0; i < INV_CODE_LENGTH; i++) {
//                    // 循环邀请码数组下标的位数与当前分组的个数整除, 则说明需要更换数组中的值
//                    if (i % actionGroup == 0) {
//                        roll = 0;
//                    } else {
//                        roll++;
//                    }
//                    T = groupItem[roll];
//                    if (preorder == 0) {
//                        arr[i] = T;
//                    } else if (preorder == 1) {
//                        arr[INV_CODE_LENGTH - 1 - i] = T;
//                    }
//                }
//                break;
//            }
            /**
             * 每组至少3个顺序元素,最多5个元素,最多2组
             * 长度7, 分组4
             * 1 | ABC ABC A
             * 2 | ABCD ABC
             * 3 | ABCDE AB
             * 4 | ABCDEF A
             *
             * 长度8, 分组5
             * 1 | ABC ABC AB
             * 2 | ABCD ABCD
             * 3 | ABCDE ABC
             * 4 | ABCDEF AB
             * 5 | ABCDEFG A
             *
             * 长度9, 分组6
             * 1 | ABC ABC ABA
             * 2 | ABCD ABCD A
             * 3 | ABCDE ABCD
             * 4 | ABCDEF ABC
             * 5 | ABCDEFG AB
             * 6 | ABCDEFGH B
             *
             * 长度10, 分组7
             * 1 | ABC ABC ABC A
             * 2 | ABCD ABCD AB
             * 3 | ABCDE ABCDE
             * 4 | ABCEDF ABCD
             * 5 | ABCDEFG ABC
             * 6 | ABCDEFGH AB
             * 7 | ABCDEFGHI A
             */
            case ABCABC: {
                Random r = new Random();
                // 交叉组合的数量,至少需要3个一组,2个一组的情况与[AB]相同
                int groupNum = INV_CODE_LENGTH - 3;
                // 分组的阈值,即每组长度,每组至少3个元素,最多2组,即[3-9]
                int actionGroup = r.nextInt(groupNum) + 3;
                // 每组的元素最多5个,多余5个不容易记忆,则没有靓号的意义
                if (actionGroup > 4) {
                    actionGroup = 4;
                }
                // 每组的元素,例如: [A,C,X]
                int groupItem[] = new int[actionGroup];
                groupItem[0] = r.nextInt(RANDOM_RANGE);
                for (int i = 1; i < groupItem.length; i++) {
                    if (groupItem[i - 1] == RANDOM_RANGE - 1) {
                        groupItem[i] = 0;
                    } else {
                        groupItem[i] = groupItem[i - 1] + 1;
                    }
                }
                int T = groupItem[0];
                // 0:正序;1:倒序;
                int preorder = r.nextInt(2);
                int roll = 0; // groupItem的位置
                for (int i = 0; i < INV_CODE_LENGTH; i++) {
                    // 循环邀请码数组下标的位数与当前分组的个数整除, 则说明需要更换数组中的值
                    if (i % actionGroup == 0) {
                        roll = 0;
                    } else {
                        roll++;
                    }
                    T = groupItem[roll];
                    if (preorder == 0) {
                        arr[i] = T;
                    } else if (preorder == 1) {
                        arr[INV_CODE_LENGTH - 1 - i] = T;
                    }
                }
                break;
            }
            /**
             * 顺序递增
             */
            case ABC: {
                Random r1 = new Random();
                int A = r1.nextInt(RANDOM_RANGE);
                for (int i = 0; i < INV_CODE_LENGTH; i++) {
                    int index = A + i;
                    // 随机数递增后需要重置为0
                    if (index > RANDOM_RANGE - 1) {
                        index = (index - RANDOM_RANGE);
                    }
                    arr[i] = index;
                }
                break;
            }
            /**
             * 纯随机生成
             */
            case RANDOM: {
                Random r2 = new Random();
                for (int i = 0; i < INV_CODE_LENGTH; i++) {
                    arr[i] = r2.nextInt(RANDOM_RANGE);
                }
                break;
            }
        }

        return arr;
    }

    /**
     * 判断以函数方式传入,减少重复代码
     *
     * @param predicate
     * @return 内容为 CODE_ITEM 的数组
     */
    private static int[] getCodeArray(Predicate<Integer> predicate) {
        int[] arr = new int[INV_CODE_LENGTH];
        Random r = new Random();
        int A = r.nextInt(RANDOM_RANGE);
        int B = reGen(A, r);
        for (int i = 0; i < INV_CODE_LENGTH; i++) {
            arr[i] = A;
            if (predicate != null) {
                if (predicate.test(i)) {
                    arr[i] = B;
                }
            }
        }
        return arr;
    }

    /**
     * 生成一个与A不同的随机数,若相同重新生成
     *
     * @param a A
     * @param r Random
     * @return B
     */
    private static int reGen(int a, Random r) {
        int b = r.nextInt(RANDOM_RANGE);
        if (b == a) {
            return reGen(a, r);
        }
        return b;
    }


    public static void main(String[] args) {
//        for (int i = 0 ; i < 1000000; i++) {
//            System.out.println(getInvCode(GoodCode.ABCABC));
//        }
        Set set = new HashSet();
        for (int i = 0; i < 10000000; i++) {
            set.add(getInvCode(true));
        }
        System.out.println(set.size());
//        System.out.println(Arrays.toString(set.toArray()));
    }

    enum GoodCode {
        /**
         * 全部相同
         */
        AAA,
        /**
         * 尾部不同, 例如:
         * AAAAAAAB
         * AAAAAABB
         * AAAAABBB
         * <p>
         * 偶数包含左右平分的情况
         */
        XXY,
        /**
         * 头部不同, 例如:
         * ABBBBBBB
         * AABBBBBB
         * AAABBBBB
         * <p>
         * 偶数包含左右平分的情况
         */
        YXX,
        /**
         * 左右不同, 奇数包含头部不同的情况,奇数的情况下即为 [ABB]
         */
        XY,
        /**
         * 头尾与中间不同, 例如:
         * ABBBBBBA
         * AABBBBAA
         * AAABBAAA
         * 防止出现 [AB] 的情况
         */
        XYX,
        /**
         * 交叉不同, 例如:
         * ABABABAB
         * AABBAABB
         * AAABBBAA
         * AABBBAAA
         * <p>
         * 包含左右平分的情况
         */
        XYXY,
        /**
         * 3个及以上交叉不同 [将大幅增加靓号数]
         * AABBCC
         * ABCABC
         */
        XYZXYZ,
        /**
         * 顺序排序, 例如:
         * ABCDEFGH
         * XYZ12345
         */
        ABC,
        /**
         * 顺序递减 [不容易记忆暂未实现]
         */
        CBA,
        /**
         * 顺序递增
         */
        ABCABC,
        /**
         * 非靓号, 可能生成靓号, 可提前生成靓号
         */
        RANDOM;
    }

}
